package paulscode.sound;

import java.io.IOException;
import java.net.URL;
import java.util.LinkedList;
import java.util.ListIterator;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;

/**
 * The MidiChannel class provides an interface for playing MIDI files, using the
 * JavaSound API. For more information about the JavaSound API, visit
 * http://java.sun.com/products/java-media/sound/ <br>
 * <br>
 * <b><i> SoundSystem License:</b></i><br>
 * <b><br>
 * You are free to use this library for any purpose, commercial or otherwise.
 * You may modify this library or source code, and distribute it any way you
 * like, provided the following conditions are met: <br>
 * 1) You may not falsely claim to be the author of this library or any
 * unmodified portion of it. <br>
 * 2) You may not copyright this library or a modified version of it and then
 * sue me for copyright infringement. <br>
 * 3) If you modify the source code, you must clearly document the changes made
 * before redistributing the modified source code, so other users know it is not
 * the original code. <br>
 * 4) You are not required to give me credit for this library in any derived
 * work, but if you do, you must also mention my website:
 * http://www.paulscode.com <br>
 * 5) I the author will not be responsible for any damages (physical, financial,
 * or otherwise) caused by the use if this library or any part of it. <br>
 * 6) I the author do not guarantee, warrant, or make any representations,
 * either expressed or implied, regarding the use of this library or any part of
 * it. <br>
 * <br>
 * Author: Paul Lamb <br>
 * http://www.paulscode.com </b>
 */
public class MidiChannel implements MetaEventListener {
	/**
	 * Processes status messages, warnings, and error messages.
	 */
	private SoundSystemLogger logger;

	/**
	 * Filename/URL to the file:
	 */
	private FilenameURL filenameURL;

	/**
	 * Unique source identifier for this MIDI source.
	 */
	private String sourcename;

	/**
	 * Global identifier for the MIDI "change volume" event.
	 */
	private static final int CHANGE_VOLUME = 7;

	/**
	 * Global identifier for the MIDI "end of track" event.
	 */
	private static final int END_OF_TRACK = 47;

	/**
	 * Used to return a current value from one of the synchronized boolean-interface
	 * methods.
	 */
	private static final boolean GET = false;

	/**
	 * Used to set the value in one of the synchronized boolean-interface methods.
	 */
	private static final boolean SET = true;

	/**
	 * Used when a parameter for one of the synchronized boolean-interface methods
	 * is not aplicable.
	 */
	private static final boolean XXX = false;

	/**
	 * Runs the assigned sequence, passing information on to the synthesizer for
	 * playback.
	 */
	private Sequencer sequencer = null;

	/**
	 * Converts MIDI events into audio.
	 */
	private Synthesizer synthesizer = null;

	/**
	 * Converts MIDI events into audio if there is no default Synthesizer.
	 */
	private MidiDevice synthDevice = null;

	/**
	 * Sequence of MIDI events defining sound.
	 */
	private Sequence sequence = null;

	/**
	 * Should playback loop or play only once.
	 */
	private boolean toLoop = true;

	/**
	 * Playback volume, float value (0.0f - 1.0f).
	 */
	private float gain = 1.0f;

	/**
	 * True while sequencer is busy being set up.
	 */
	private boolean loading = true;

	/**
	 * The list of MIDI files to play when the current sequence finishes.
	 */
	private LinkedList<FilenameURL> sequenceQueue = null;

	/**
	 * Ensures that only one thread accesses the sequenceQueue at a time.
	 */
	private final Object sequenceQueueLock = new Object();

	/**
	 * Specifies the gain factor used for the fade-out effect, or -1 when playback
	 * is not currently fading out.
	 */
	protected float fadeOutGain = -1.0f;

	/**
	 * Specifies the gain factor used for the fade-in effect, or 1 when playback is
	 * not currently fading in.
	 */
	protected float fadeInGain = 1.0f;

	/**
	 * Specifies the number of miliseconds it should take to fade out.
	 */
	protected long fadeOutMilis = 0;

	/**
	 * Specifies the number of miliseconds it should take to fade in.
	 */
	protected long fadeInMilis = 0;

	/**
	 * System time in miliseconds when the last fade in/out volume check occurred.
	 */
	protected long lastFadeCheck = 0;

	/**
	 * Used for fading in and out effects.
	 */
	private FadeThread fadeThread = null;

	/**
	 * Constructor: Defines the basic source information.
	 * 
	 * @param toLoop     Should playback loop or play only once?
	 * @param sourcename Unique identifier for this source.
	 * @param filename   Name of the MIDI file to play.
	 */
	public MidiChannel(boolean toLoop, String sourcename, String filename) {
		// let others know we are busy loading:
		loading(SET, true);

		// grab a handle to the message logger:
		logger = SoundSystemConfig.getLogger();

		// save information about the source:
		filenameURL(SET, new FilenameURL(filename));
		sourcename(SET, sourcename);
		setLooping(toLoop);

		// initialize the MIDI channel:
		init();

		// finished loading:
		loading(SET, false);
	}

	/**
	 * Constructor: Defines the basic source information. The fourth parameter,
	 * 'identifier' should look like a filename, and it must have the correct
	 * extension (.mid or .midi).
	 * 
	 * @param toLoop     Should playback loop or play only once?
	 * @param sourcename Unique identifier for this source.
	 * @param midiFile   URL to the MIDI file to play.
	 * @param identifier Filename/identifier for the MIDI file.
	 */
	public MidiChannel(boolean toLoop, String sourcename, URL midiFile, String identifier) {
		// let others know we are busy loading
		loading(SET, true);

		// grab a handle to the message logger:
		logger = SoundSystemConfig.getLogger();

		// save information about the source:
		filenameURL(SET, new FilenameURL(midiFile, identifier));
		sourcename(SET, sourcename);
		setLooping(toLoop);

		// initialize the MIDI channel:
		init();

		// finished loading:
		loading(SET, false);
	}

	/**
	 * Constructor: Defines the basic source information.
	 * 
	 * @param toLoop          Should playback loop or play only once?
	 * @param sourcename      Unique identifier for this source.
	 * @param midiFilenameURL Filename/URL to the MIDI file to play.
	 */
	public MidiChannel(boolean toLoop, String sourcename, FilenameURL midiFilenameURL) {
		// let others know we are busy loading
		loading(SET, true);

		// grab a handle to the message logger:
		logger = SoundSystemConfig.getLogger();

		// save information about the source:
		filenameURL(SET, midiFilenameURL);
		sourcename(SET, sourcename);
		setLooping(toLoop);

		// initialize the MIDI channel:
		init();

		// finished loading:
		loading(SET, false);
	}

	/**
	 * Initializes the sequencer, loads the sequence, and sets up the synthesizer.
	 */
	private void init() {
		// Load a sequencer:
		getSequencer();

		// Load the sequence to play:
		setSequence(filenameURL(GET, null).getURL());

		// Load a synthesizer to play the sequence on:
		getSynthesizer();

		// Ensure the initial volume is correct:
		// (TODO: doesn't always work??)
		resetGain();
	}

	/**
	 * Shuts the channel down and removes references to all instantiated objects.
	 */
	public void cleanup() {
		loading(SET, true);
		setLooping(true);

		if (sequencer != null) {
			try {
				sequencer.stop();
				sequencer.close();
				sequencer.removeMetaEventListener(this);
			} catch (Exception e) {
			}
		}

		logger = null;
		sequencer = null;
		synthesizer = null;
		sequence = null;

		synchronized (sequenceQueueLock) {
			if (sequenceQueue != null)
				sequenceQueue.clear();
			sequenceQueue = null;
		}

		// End the fade effects thread if it exists:
		if (fadeThread != null) {
			boolean killException = false;
			try {
				fadeThread.kill(); // end the fade effects thread.
				fadeThread.interrupt(); // wake the thread up so it can end.
			} catch (Exception e) {
				killException = true;
			}

			if (!killException) {
				// wait up to 5 seconds for fade effects thread to end:
				for (int i = 0; i < 50; i++) {
					if (!fadeThread.alive())
						break;
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
					}
				}
			}

			// Let user know if there was a problem ending the fade thread
			if (killException || fadeThread.alive()) {
				errorMessage("MIDI fade effects thread did not die!");
				message("Ignoring errors... continuing clean-up.");
			}
		}

		fadeThread = null;

		loading(SET, false);
	}

	/**
	 * Queues up the next MIDI sequence to play when the previous sequence ends.
	 * 
	 * @param filenameURL MIDI sequence to play next.
	 */
	public void queueSound(FilenameURL filenameURL) {
		if (filenameURL == null) {
			errorMessage("Filename/URL not specified in method 'queueSound'");
			return;
		}

		synchronized (sequenceQueueLock) {
			if (sequenceQueue == null)
				sequenceQueue = new LinkedList<FilenameURL>();
			sequenceQueue.add(filenameURL);
		}
	}

	/**
	 * Removes the first occurrence of the specified filename/identifier from the
	 * list of MIDI sequences to play when the previous sequence ends.
	 * 
	 * @param filename Filename or identifier of a MIDI sequence to remove from the
	 *                 queue.
	 */
	public void dequeueSound(String filename) {
		if (filename == null || filename.equals("")) {
			errorMessage("Filename not specified in method 'dequeueSound'");
			return;
		}

		synchronized (sequenceQueueLock) {
			if (sequenceQueue != null) {
				ListIterator<FilenameURL> i = sequenceQueue.listIterator();
				while (i.hasNext()) {
					if (i.next().getFilename().equals(filename)) {
						i.remove();
						break;
					}
				}
			}
		}
	}

	/**
	 * Fades out the volume of whatever sequence is currently playing, then begins
	 * playing the specified MIDI file at the previously assigned volume level. If
	 * the filenameURL parameter is null or empty, playback will simply fade out and
	 * stop. The miliseconds parameter must be non-negative or zero. This method
	 * will remove anything that is currently in the list of queued MIDI sequences
	 * that would have played next when current playback finished.
	 * 
	 * @param filenameURL MIDI file to play next, or null for none.
	 * @param milis       Number of miliseconds the fadeout should take.
	 */
	public void fadeOut(FilenameURL filenameURL, long milis) {
		if (milis < 0) {
			errorMessage("Miliseconds may not be negative in method " + "'fadeOut'.");
			return;
		}

		fadeOutMilis = milis;
		fadeInMilis = 0;
		fadeOutGain = 1.0f;
		lastFadeCheck = System.currentTimeMillis();

		synchronized (sequenceQueueLock) {
			if (sequenceQueue != null)
				sequenceQueue.clear();

			if (filenameURL != null) {
				if (sequenceQueue == null)
					sequenceQueue = new LinkedList<FilenameURL>();
				sequenceQueue.add(filenameURL);
			}
		}
		if (fadeThread == null) {
			fadeThread = new FadeThread();
			fadeThread.start();
		}
		fadeThread.interrupt();
	}

	/**
	 * Fades out the volume of whatever sequence is currently playing, then fades
	 * the volume back in playing the specified MIDI file. Final volume after
	 * fade-in completes will be equal to the previously assigned volume level. The
	 * filenameURL parameter may not be null or empty. The miliseconds parameters
	 * must be non-negative or zero. This method will remove anything that is
	 * currently in the list of queued MIDI sequences that would have played next
	 * when current playback finished.
	 * 
	 * @param filenameURL MIDI file to play next, or null for none.
	 * @param milisOut    Number of miliseconds the fadeout should take.
	 * @param milisIn     Number of miliseconds the fadein should take.
	 */
	public void fadeOutIn(FilenameURL filenameURL, long milisOut, long milisIn) {
		if (filenameURL == null) {
			errorMessage("Filename/URL not specified in method 'fadeOutIn'.");
			return;
		}
		if (milisOut < 0 || milisIn < 0) {
			errorMessage("Miliseconds may not be negative in method " + "'fadeOutIn'.");
			return;
		}

		fadeOutMilis = milisOut;
		fadeInMilis = milisIn;
		fadeOutGain = 1.0f;
		lastFadeCheck = System.currentTimeMillis();

		synchronized (sequenceQueueLock) {
			if (sequenceQueue == null)
				sequenceQueue = new LinkedList<FilenameURL>();
			sequenceQueue.clear();
			sequenceQueue.add(filenameURL);
		}
		if (fadeThread == null) {
			fadeThread = new FadeThread();
			fadeThread.start();
		}
		fadeThread.interrupt();
	}

	/**
	 * Resets this source's volume if it is fading out or in. Returns true if this
	 * source is currently in the process of fading out. When fade-out completes,
	 * this method transitions the source to the next sound in the sound sequence
	 * queue if there is one. This method has no effect on non-streaming sources.
	 * 
	 * @return True if this source is in the process of fading out.
	 */
	private synchronized boolean checkFadeOut() {
		if (fadeOutGain == -1.0f && fadeInGain == 1.0f)
			return false;

		long currentTime = System.currentTimeMillis();
		long milisPast = currentTime - lastFadeCheck;
		lastFadeCheck = currentTime;

		if (fadeOutGain >= 0.0f) {
			if (fadeOutMilis == 0) {
				fadeOutGain = 0.0f;
				fadeInGain = 0.0f;
				if (!incrementSequence())
					stop();
				rewind();
				resetGain();
				return false;
			} else {
				float fadeOutReduction = ((float) milisPast) / ((float) fadeOutMilis);

				fadeOutGain -= fadeOutReduction;
				if (fadeOutGain <= 0.0f) {
					fadeOutGain = -1.0f;
					fadeInGain = 0.0f;
					if (!incrementSequence())
						stop();
					rewind();
					resetGain();
					return false;
				}
			}
			resetGain();
			return true;
		}

		if (fadeInGain < 1.0f) {
			fadeOutGain = -1.0f;
			if (fadeInMilis == 0) {
				fadeOutGain = -1.0f;
				fadeInGain = 1.0f;
			} else {
				float fadeInIncrease = ((float) milisPast) / ((float) fadeInMilis);
				fadeInGain += fadeInIncrease;
				if (fadeInGain >= 1.0f) {
					fadeOutGain = -1.0f;
					fadeInGain = 1.0f;
				}
			}
			resetGain();
		}

		return false;
	}

	/**
	 * Removes the next sequence from the queue and assigns it to the sequencer.
	 * 
	 * @return True if there was something in the queue.
	 */
	private boolean incrementSequence() {
		synchronized (sequenceQueueLock) {
			// Is there a queue, and if so, is there anything in it:
			if (sequenceQueue != null && sequenceQueue.size() > 0) {
				// grab the next filename/URL from the queue:
				filenameURL(SET, sequenceQueue.remove(0));

				// Let everyone know we are busy loading:
				loading(SET, true);

				// Check if we have a sequencer:
				if (sequencer == null) {
					// nope, try and get one now:
					getSequencer();
				} else {
					// We have a sequencer. Stop it now:
					sequencer.stop();
					// rewind to the beginning:
					sequencer.setMicrosecondPosition(0);
					// Stop listening for a moment:
					sequencer.removeMetaEventListener(this);
					// wait a bit for the sequencer to shut down and rewind:
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
					}
				}
				// We need to have a sequencer at this point:
				if (sequencer == null) {
					errorMessage("Unable to set the sequence in method " + "'incrementSequence', because there wasn't "
							+ "a sequencer to use.");

					// Finished loading:
					loading(SET, false);

					// failure:
					return false;
				}
				// set the new sequence to be played:
				setSequence(filenameURL(GET, null).getURL());
				// start playing again:
				sequencer.start();
				// make sure we play at the correct volume:
				// (TODO: This doesn't always work??)
				resetGain();
				// start listening for end of track event again:
				sequencer.addMetaEventListener(this);

				// Finished loading:
				loading(SET, false);

				// We successfully moved to the next sequence:
				return true;
			}
		}

		// Nothing left to load
		return false;
	}

	/**
	 * Plays the MIDI file from the beginning, or from where it left off if it was
	 * paused.
	 */
	public void play() {
		if (!loading()) {
			// Make sure there is a sequencer:
			if (sequencer == null)
				return;

			try {
				// start playing:
				sequencer.start();
				// event will be sent when end of track is reached:
				sequencer.addMetaEventListener(this);
			} catch (Exception e) {
				errorMessage("Exception in method 'play'");
				printStackTrace(e);
				SoundSystemException sse = new SoundSystemException(e.getMessage());
				SoundSystem.setException(sse);
			}
		}
	}

	/**
	 * Stops playback and rewinds to the beginning.
	 */
	public void stop() {
		if (!loading()) {
			// Make sure there is a sequencer:
			if (sequencer == null)
				return;

			try {
				// stop playback:
				sequencer.stop();
				// rewind to the beginning:
				sequencer.setMicrosecondPosition(0);
				// No need to listen any more:
				sequencer.removeMetaEventListener(this);
			} catch (Exception e) {
				errorMessage("Exception in method 'stop'");
				printStackTrace(e);
				SoundSystemException sse = new SoundSystemException(e.getMessage());
				SoundSystem.setException(sse);
			}
		}
	}

	/**
	 * Temporarily stops playback without rewinding.
	 */
	public void pause() {
		if (!loading()) {
			// Make sure there is a sequencer:
			if (sequencer == null)
				return;

			try {
				// stop playback. Will resume from this location next play.
				sequencer.stop();
			} catch (Exception e) {
				errorMessage("Exception in method 'pause'");
				printStackTrace(e);
				SoundSystemException sse = new SoundSystemException(e.getMessage());
				SoundSystem.setException(sse);
			}
		}
	}

	/**
	 * Returns playback to the beginning.
	 */
	public void rewind() {
		if (!loading()) {
			// Make sure there is a sequencer:
			if (sequencer == null)
				return;

			try {
				// rewind to the beginning:
				sequencer.setMicrosecondPosition(0);
			} catch (Exception e) {
				errorMessage("Exception in method 'rewind'");
				printStackTrace(e);
				SoundSystemException sse = new SoundSystemException(e.getMessage());
				SoundSystem.setException(sse);
			}
		}
	}

	/**
	 * Changes the volume of MIDI playback.
	 * 
	 * @param value Float value (0.0f - 1.0f).
	 */
	public void setVolume(float value) {
		gain = value;
		resetGain();
	}

	/**
	 * Returns the current volume for the MIDI source.
	 * 
	 * @return Float value (0.0f - 1.0f).
	 */
	public float getVolume() {
		return gain;
	}

	/**
	 * Changes the basic information about the MIDI source. This method removes any
	 * queued filenames/URLs from the list of MIDI sequences that would have played
	 * after the current sequence ended.
	 * 
	 * @param toLoop     Should playback loop or play only once?
	 * @param sourcename Unique identifier for this source.
	 * @param filename   Name of the MIDI file to play.
	 */
	public void switchSource(boolean toLoop, String sourcename, String filename) {
		// Let everyone know we are busy loading:
		loading(SET, true);

		// save information about the source:
		filenameURL(SET, new FilenameURL(filename));
		sourcename(SET, sourcename);
		setLooping(toLoop);

		reset();

		// Finished loading:
		loading(SET, false);
	}

	/**
	 * Changes the basic information about the MIDI source. This method removes any
	 * queued filenames/URLs from the list of MIDI sequences that would have played
	 * after the current sequence ended. The fourth parameter, 'identifier' should
	 * look like a filename, and it must have the correct extension (.mid or .midi).
	 * 
	 * @param toLoop     Should playback loop or play only once?
	 * @param sourcename Unique identifier for this source.
	 * @param midiFile   URL to the MIDI file to play.
	 * @param identifier Filename/identifier for the MIDI file.
	 */
	public void switchSource(boolean toLoop, String sourcename, URL midiFile, String identifier) {
		// Let everyone know we are busy loading:
		loading(SET, true);

		// save information about the source:
		filenameURL(SET, new FilenameURL(midiFile, identifier));
		sourcename(SET, sourcename);
		setLooping(toLoop);

		reset();

		// Finished loading:
		loading(SET, false);
	}

	/**
	 * Changes the basic information about the MIDI source. This method removes any
	 * queued filenames/URLs from the list of MIDI sequences that would have played
	 * after the current sequence ended.
	 * 
	 * @param toLoop      Should playback loop or play only once?
	 * @param sourcename  Unique identifier for this source.
	 * @param filenameURL Filename/URL of the MIDI file to play.
	 */
	public void switchSource(boolean toLoop, String sourcename, FilenameURL filenameURL) {
		// Let everyone know we are busy loading:
		loading(SET, true);

		// save information about the source:
		filenameURL(SET, filenameURL);
		sourcename(SET, sourcename);
		setLooping(toLoop);

		reset();

		// Finished loading:
		loading(SET, false);
	}

	/**
	 * Stops and rewinds the sequencer, and resets the sequence.
	 */
	private void reset() {
		synchronized (sequenceQueueLock) {
			if (sequenceQueue != null)
				sequenceQueue.clear();
		}

		// Check if we have a sequencer:
		if (sequencer == null) {
			// nope, try and get one now:
			getSequencer();
		} else {
			// We have a sequencer. Stop it now:
			sequencer.stop();
			// rewind to the beginning:
			sequencer.setMicrosecondPosition(0);
			// Stop listening for a moment:
			sequencer.removeMetaEventListener(this);
			// wait a bit for the sequencer to shut down and rewind:
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
			}
		}
		// We need to have a sequencer at this point:
		if (sequencer == null) {
			errorMessage(
					"Unable to set the sequence in method " + "'reset', because there wasn't " + "a sequencer to use.");
			return;
		}

		// set the new sequence to be played:
		setSequence(filenameURL(GET, null).getURL());
		// start playing again:
		sequencer.start();
		// make sure we play at the correct volume:
		// (TODO: This doesn't always work??)
		resetGain();
		// start listening for end of track event again:
		sequencer.addMetaEventListener(this);
	}

	/**
	 * Sets the value of boolean 'toLoop'.
	 * 
	 * @param value True or False.
	 */
	public void setLooping(boolean value) {
		toLoop(SET, value);
	}

	/**
	 * Returns the value of boolean 'toLoop'.
	 * 
	 * @return True while looping.
	 */
	public boolean getLooping() {
		return toLoop(GET, XXX);
	}

	/**
	 * Sets or returns the value of boolean 'toLoop'.
	 * 
	 * @param action GET or SET.
	 * @param value  New value if action == SET, or XXX if action == GET.
	 * @return True while looping.
	 */
	private synchronized boolean toLoop(boolean action, boolean value) {
		if (action == SET)
			toLoop = value;
		return toLoop;
	}

	/**
	 * Check if a MIDI file is in the process of loading.
	 */
	public boolean loading() {
		return (loading(GET, XXX));
	}

	/**
	 * Sets or returns the value of boolean 'loading'.
	 * 
	 * @param action GET or SET.
	 * @param value  New value if action == SET, or XXX if action == GET.
	 * @return True while a MIDI file is in the process of loading.
	 */
	private synchronized boolean loading(boolean action, boolean value) {
		if (action == SET)
			loading = value;
		return loading;
	}

	/**
	 * Defines the unique identifier for this source
	 * 
	 * @param value New source name.
	 */
	public void setSourcename(String value) {
		sourcename(SET, value);
	}

	/**
	 * Returns the unique identifier for this source.
	 * 
	 * @return The source's name.
	 */
	public String getSourcename() {
		return sourcename(GET, null);
	}

	/**
	 * Sets or returns the value of String 'sourcename'.
	 * 
	 * @param action GET or SET.
	 * @param value  New value if action == SET, or null if action == GET.
	 * @return The source's name.
	 */
	private synchronized String sourcename(boolean action, String value) {
		if (action == SET)
			sourcename = value;
		return sourcename;
	}

	/**
	 * Defines which MIDI file to play.
	 * 
	 * @param value Path to the MIDI file.
	 */
	public void setFilenameURL(FilenameURL value) {
		filenameURL(SET, value);
	}

	/**
	 * Returns the filename/identifier of the MIDI file being played.
	 * 
	 * @return Filename of identifier of the MIDI file.
	 */
	public String getFilename() {
		return filenameURL(GET, null).getFilename();
	}

	/**
	 * Returns the MIDI file being played.
	 * 
	 * @return Filename/URL of the MIDI file.
	 */
	public FilenameURL getFilenameURL() {
		return filenameURL(GET, null);
	}

	/**
	 * Sets or returns the value of filenameURL.
	 * 
	 * @param action GET or SET.
	 * @param value  New value if action == SET, or null if action == GET.
	 * @return Path to the MIDI file.
	 */
	private synchronized FilenameURL filenameURL(boolean action, FilenameURL value) {
		if (action == SET)
			filenameURL = value;
		return filenameURL;
	}

	/**
	 * Called when MIDI events occur.
	 * 
	 * @param message Meta mssage describing the MIDI event.
	 */
	public void meta(MetaMessage message) {
		if (message.getType() == END_OF_TRACK) {
			// Generate an EOS event:
			SoundSystemConfig.notifyEOS(sourcename, sequenceQueue.size());

			// check if we should loop or not:
			if (toLoop) {
				// looping
				// Check if playback is in the process of fading out.
				if (!checkFadeOut()) {
					// Not fading out, progress to the next MIDI sequence if
					// any are queued.
					if (!incrementSequence()) {
						try {
							// Rewind to the beginning.
							sequencer.setMicrosecondPosition(0);
							sequencer.start();
							// Make sure playback volume is correct.
							resetGain();
						} catch (Exception e) {
						}
					}
				} else if (sequencer != null) {
					try {
						// Rewind to the beginning.
						sequencer.setMicrosecondPosition(0);
						sequencer.start();
						// Make sure playback volume is correct.
						resetGain();
					} catch (Exception e) {
					}
				}
			} else {
				// non-looping
				if (!checkFadeOut()) {
					if (!incrementSequence()) {
						try {
							// stop playback:
							sequencer.stop();
							// rewind to the beginning:
							sequencer.setMicrosecondPosition(0);
							// stop looping:
							sequencer.removeMetaEventListener(this);
						} catch (Exception e) {
						}
					}
				} else {
					try {
						// stop playback:
						sequencer.stop();
						// rewind to the beginning:
						sequencer.setMicrosecondPosition(0);
						// stop looping:
						sequencer.removeMetaEventListener(this);
					} catch (Exception e) {
					}
				}
			}
		}
	}

	/**
	 * Resets playback volume to the correct level.
	 */
	public void resetGain() {
		// make sure the value for gain is valid (between 0 and 1)
		if (gain < 0.0f)
			gain = 0.0f;
		if (gain > 1.0f)
			gain = 1.0f;

		int midiVolume = (int) (gain * SoundSystemConfig.getMasterGain() * (float) Math.abs(fadeOutGain) * fadeInGain
				* 127.0f);
		if (synthesizer != null) {
			javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels();
			for (int c = 0; channels != null && c < channels.length; c++) {
				channels[c].controlChange(CHANGE_VOLUME, midiVolume);
			}
		} else if (synthDevice != null) {
			try {
				ShortMessage volumeMessage = new ShortMessage();
				for (int i = 0; i < 16; i++) {
					volumeMessage.setMessage(ShortMessage.CONTROL_CHANGE, i, CHANGE_VOLUME, midiVolume);
					synthDevice.getReceiver().send(volumeMessage, -1);
				}
			} catch (Exception e) {
				errorMessage("Error resetting gain on MIDI device");
				printStackTrace(e);
			}
		} else if (sequencer != null && sequencer instanceof Synthesizer) {
			synthesizer = (Synthesizer) sequencer;
			javax.sound.midi.MidiChannel[] channels = synthesizer.getChannels();
			for (int c = 0; channels != null && c < channels.length; c++) {
				channels[c].controlChange(CHANGE_VOLUME, midiVolume);
			}
		} else {
			try {
				Receiver receiver = MidiSystem.getReceiver();
				ShortMessage volumeMessage = new ShortMessage();
				for (int c = 0; c < 16; c++) {
					volumeMessage.setMessage(ShortMessage.CONTROL_CHANGE, c, CHANGE_VOLUME, midiVolume);
					receiver.send(volumeMessage, -1);
				}
			} catch (Exception e) {
				errorMessage("Error resetting gain on default receiver");
				printStackTrace(e);
			}
		}
	}

	/**
	 * Attempts to load the default sequencer. If it fails, then other common
	 * sequencers are tried. If none can be loaded, then variable 'sequencer'
	 * remains null.
	 */
	private void getSequencer() {
		try {
			sequencer = MidiSystem.getSequencer();
			if (sequencer != null) {
				try {
					sequencer.getTransmitter();
				} catch (MidiUnavailableException mue) {
					message("Unable to get a transmitter from the " + "default MIDI sequencer");
				}
				sequencer.open();
			}
		} catch (MidiUnavailableException mue) {
			message("Unable to open the default MIDI sequencer");
			sequencer = null;
		} catch (Exception e) {
			if (e instanceof InterruptedException) {
				message("Caught InterruptedException while attempting to "
						+ "open the default MIDI sequencer.  Trying again.");
				sequencer = null;
			}
			try {
				sequencer = MidiSystem.getSequencer();
				if (sequencer != null) {
					try {
						sequencer.getTransmitter();
					} catch (MidiUnavailableException mue) {
						message("Unable to get a transmitter from the " + "default MIDI sequencer");
					}
					sequencer.open();
				}
			} catch (MidiUnavailableException mue) {
				message("Unable to open the default MIDI sequencer");
				sequencer = null;
			} catch (Exception e2) {
				message("Unknown error opening the default MIDI sequencer");
				sequencer = null;
			}
		}

		if (sequencer == null)
			sequencer = openSequencer("Real Time Sequencer");
		if (sequencer == null)
			sequencer = openSequencer("Java Sound Sequencer");
		if (sequencer == null) {
			errorMessage("Failed to find an available MIDI sequencer");
			return;
		}
	}

	/**
	 * Loads the MIDI sequence form the specified URL, and sets the sequence. If
	 * variable 'sequencer' is null or an error occurs, then variable 'sequence'
	 * remains null.
	 * 
	 * @param midiSource URL to a MIDI file.
	 */
	private void setSequence(URL midiSource) {
		if (sequencer == null) {
			errorMessage("Unable to update the sequence in method " + "'setSequence', because variable 'sequencer' "
					+ "is null");
			return;
		}

		if (midiSource == null) {
			errorMessage("Unable to load Midi file in method 'setSequence'.");
			return;
		}

		try {
			sequence = MidiSystem.getSequence(midiSource);
		} catch (IOException ioe) {
			errorMessage("Input failed while reading from MIDI file in " + "method 'setSequence'.");
			printStackTrace(ioe);
			return;
		} catch (InvalidMidiDataException imde) {
			errorMessage("Invalid MIDI data encountered, or not a MIDI " + "file in method 'setSequence' (1).");
			printStackTrace(imde);
			return;
		}
		if (sequence == null) {
			errorMessage("MidiSystem 'getSequence' method returned null " + "in method 'setSequence'.");
		} else {
			try {
				sequencer.setSequence(sequence);
			} catch (InvalidMidiDataException imde) {
				errorMessage("Invalid MIDI data encountered, or not a MIDI " + "file in method 'setSequence' (2).");
				printStackTrace(imde);
				return;
			} catch (Exception e) {
				errorMessage("Problem setting sequence from MIDI file in " + "method 'setSequence'.");
				printStackTrace(e);
				return;
			}
		}
	}

	/**
	 * First attempts to load the specified "override MIDI synthesizer" if one was
	 * defined. If none was defined or unable to use it, then attempts to load the
	 * default synthesizer. If that fails, then other common synthesizers are
	 * attempted. If none can be loaded, then MIDI is not possible on this system.
	 */
	private void getSynthesizer() {
		if (sequencer == null) {
			errorMessage("Unable to load a Synthesizer in method " + "'getSynthesizer', because variable 'sequencer' "
					+ "is null");
			return;
		}

		// Check if an alternate MIDI synthesizer was specified to use
		String overrideMIDISynthesizer = SoundSystemConfig.getOverrideMIDISynthesizer();
		if (overrideMIDISynthesizer != null && !overrideMIDISynthesizer.equals("")) {
			// Try and open the specified device:
			synthDevice = openMidiDevice(overrideMIDISynthesizer);
			// See if we got it:
			if (synthDevice != null) {
				// Got it, try and link it to the sequencer:
				try {
					sequencer.getTransmitter().setReceiver(synthDevice.getReceiver());
					// Success!
					return;
				} catch (MidiUnavailableException mue) {
					// Problem linking the two, let the user know
					errorMessage("Unable to link sequencer transmitter " + "with receiver for MIDI device '"
							+ overrideMIDISynthesizer + "'");
				}
			}
		}

		// No alternate MIDI synthesizer was specified, or unable to use it.

		// If the squencer were also a synthesizer, that would make things easy:
		if (sequencer instanceof Synthesizer) {
			synthesizer = (Synthesizer) sequencer;
		} else {
			// Try getting the default synthesizer first:
			try {
				synthesizer = MidiSystem.getSynthesizer();
				synthesizer.open();
			} catch (MidiUnavailableException mue) {
				message("Unable to open the default synthesizer");
				synthesizer = null;
			}

			// See if we were sucessful:
			if (synthesizer == null) {
				// Try for the common MIDI synthesizers:
				synthDevice = openMidiDevice("Java Sound Synthesizer");
				if (synthDevice == null)
					synthDevice = openMidiDevice("Microsoft GS Wavetable");
				if (synthDevice == null)
					synthDevice = openMidiDevice("Gervill");
				if (synthDevice == null) {
					// Still nothing, MIDI is not going to work
					errorMessage("Failed to find an available MIDI " + "synthesizer");
					return;
				}
			}

			// Are we using the default synthesizer or something else?
			if (synthesizer == null) {
				// Link the sequencer and synthesizer:
				try {
					sequencer.getTransmitter().setReceiver(synthDevice.getReceiver());
				} catch (MidiUnavailableException mue) {
					errorMessage("Unable to link sequencer transmitter " + "with MIDI device receiver");
				}
			} else {
				// Bug-fix for multiple-receivers playing simultaneously
				if (synthesizer.getDefaultSoundbank() == null) {
					// Link the sequencer to the default receiver:
					try {
						sequencer.getTransmitter().setReceiver(MidiSystem.getReceiver());
					} catch (MidiUnavailableException mue) {
						errorMessage("Unable to link sequencer transmitter " + "with default receiver");
					}
				} else {
					// Link the sequencer to the default synthesizer:
					try {
						sequencer.getTransmitter().setReceiver(synthesizer.getReceiver());
					} catch (MidiUnavailableException mue) {
						errorMessage("Unable to link sequencer transmitter " + "with synthesizer receiver");
					}
				}
				// End bug-fix
			}
		}
	}

	/**
	 * Attempts to open the Sequencer with a name containing the specified string.
	 * 
	 * @param containsString Part or all of a Sequencer's name.
	 * @return Handle to the Sequencer, or null if not found or error.
	 */
	private Sequencer openSequencer(String containsString) {
		Sequencer s = null;
		s = (Sequencer) openMidiDevice(containsString);
		if (s == null)
			return null;
		try {
			s.getTransmitter();
		} catch (MidiUnavailableException mue) {
			message("    Unable to get a transmitter from this sequencer");
			s = null;
			return null;
		}

		return s;
	}

	/**
	 * Attempts to open the MIDI device with a name containing the specified string.
	 * 
	 * @param containsString Part or all of a MIDI device's name.
	 * @return Handle to the MIDI device, or null if not found or error.
	 */
	private MidiDevice openMidiDevice(String containsString) {
		message("Searching for MIDI device with name containing '" + containsString + "'");
		MidiDevice device = null;
		MidiDevice.Info[] midiDevices = MidiSystem.getMidiDeviceInfo();
		for (int i = 0; i < midiDevices.length; i++) {
			device = null;
			try {
				device = MidiSystem.getMidiDevice(midiDevices[i]);
			} catch (MidiUnavailableException e) {
				message("    Problem in method 'getMidiDevice':  " + "MIDIUnavailableException was thrown");
				device = null;
			}
			if (device != null && midiDevices[i].getName().contains(containsString)) {
				message("    Found MIDI device named '" + midiDevices[i].getName() + "'");
				if (device instanceof Synthesizer)
					message("        *this is a Synthesizer instance");
				if (device instanceof Sequencer)
					message("        *this is a Sequencer instance");
				try {
					device.open();
				} catch (MidiUnavailableException mue) {
					message("    Unable to open this MIDI device");
					device = null;
				}
				return device;
			}
		}
		message("    MIDI device not found");
		return null;
	}

	/**
	 * Prints a message.
	 * 
	 * @param message Message to print.
	 */
	protected void message(String message) {
		logger.message(message, 0);
	}

	/**
	 * Prints an important message.
	 * 
	 * @param message Message to print.
	 */
	protected void importantMessage(String message) {
		logger.importantMessage(message, 0);
	}

	/**
	 * Prints the specified message if error is true.
	 * 
	 * @param error   True or False.
	 * @param message Message to print if error is true.
	 * @return True if error is true.
	 */
	protected boolean errorCheck(boolean error, String message) {
		return logger.errorCheck(error, "MidiChannel", message, 0);
	}

	/**
	 * Prints an error message.
	 * 
	 * @param message Message to print.
	 */
	protected void errorMessage(String message) {
		logger.errorMessage("MidiChannel", message, 0);
	}

	/**
	 * Prints an exception's error message followed by the stack trace.
	 * 
	 * @param e Exception containing the information to print.
	 */
	protected void printStackTrace(Exception e) {
		logger.printStackTrace(e, 1);
	}

	/**
	 * The FadeThread class handles sequence changing, timing, and volume change
	 * messages in the background.
	 */
	private class FadeThread extends SimpleThread {
		@Override
		/**
		 * Runs in the background, timing fade in and fade out, changing the sequence,
		 * and issuing the appropriate volume change messages.
		 */
		public void run() {
			while (!dying()) {
				// if not currently fading in or out, put the thread to sleep
				if (fadeOutGain == -1.0f && fadeInGain == 1.0f)
					snooze(3600000);
				checkFadeOut();
				// only update every 50 miliseconds (no need to peg the cpu)
				snooze(50);
			}
			// Important!
			cleanup();
		}
	}

}
